ERB Templates as Standalone Executables
Friday 30 December 2022

ruby

The following is a technique that can convert ERB files (Embeded Ruby) to
executable files like any other ruby/python/shell scripts, the difference is
that ERB is more suitable in cases where you have some output that needs to be
filled with data, like rails view files, or a text that needs to report number of
files/dependencies, in this post I will try to explore this idea.

first I created a file with this content

Today is : <%= Time.new %>

You can render the file with erb as it’s command line tool to render erb
files and output the result to the terminal.

1erb date.erb

will output something similar to the following

Today is : 2018-01-24 21:08:20 +0100

As Linux looks for shebang in
every executable file, then if this erb file given the executable permission and
prepended by a shebang for erb it will be executed nearly the same way but
without explicitly specifying erb in the terminal, so first rename the file

1mv date.erb dateprinter

and give it the executable permission

1chmod +x dateprinter

now prepend it with a shebang for erb, it should look like this now

#!/usr/bin/env erb
Today is : <%= Time.new %>

now it could be executed as follows

1./dateprinter

the only difference is that the output will have the shebang line also, as ERB
doesn’t parse it and consider it part of the template file.

1#!/usr/bin/env erb
2Today is : 2018-01-24 21:08:20 +0100

if you put the previous file in a directory that is added to your shell $PATH
you can execute it from anywhere by it’s name like any other executable file.

1dateprinter

So in that case your shell will look for executable file in the path, will find
the dateprinter file, it’ll try to figure out how to execute it with exec so
it’ll inspect for magic bytes, it’ll find the shebang, so it’ll pass the file
to the appropriate interpreter erb and erb in that case will execute it and
hand you the output.

now lets do something useful with this idea, lets create a template that print
the number of dependencies for a rails application.

When you execute bundle install the last 2 lines looks like this

1Bundle complete! 27 Gemfile dependencies, 109 gems now installed.
2Use `bundle info [gemname]` to see where a bundled gem is installed.

so to get the appropriate line we need to cut the output with head and tail
as follows

1bundle --local | tail -n2 | head -n1

so the returned output will be similar to this

1Bundle complete! 27 Gemfile dependencies, 109 gems now installed.

now with ruby we can match the numbers in any string and extract them

1stats = `bundle --local | tail -n2 | head -n1`
2numbers = stats.scan(/[0-9]+/)
3direct_dep = numbers.first
4indirect_dep = numbers.last

so an ERB file as the following can print out these stats, I added some
sprinkles on top

 1#!/usr/bin/env erb
 2Project Name: <%= File.basename(Dir.pwd).capitalize %>
 3<% stats = `bundle --local | tail -n2 | head -n1`.scan(/[0-9]+/) %>
 4Direct Dependencies: <%= stats.first %>
 5Indirect Dpendencies: <%= stats.last %>
 6Direct Initializers:  <%= Dir.glob('config/initializers/*.rb').count %>
 7Initializers in Development Env: <%= `rake initializers`.lines.count %>
 8Initializers in Production Env: <%= `RAILS_ENV=production rake initializers`.lines.count %>
 9Controllers: <%= Dir.glob('app/controllers/**/*_controller.rb').count %>
10Models: <%= Dir.glob('app/models/**/*.*').reject{|f| f.include?('concern') }.count %>
11Views: <%= Dir.glob('app/views/**/*.*').count %>
12
13<%
14def files_for(ext)
15  Dir.glob('**/*.' + ext)
16end
17
18def size_for(ext)
19  files_for(ext).map{|f| File.size(f) }.inject(:+).to_i
20end
21%>
22Assets:
23JS: <%= files_for('js').count %> Files, <%= size_for('js') / 1024 %> KB
24SCSS: <%= files_for('scss').count %> Files, <%= size_for('scss') / 1024 %> KB
25PNG: <%= files_for('png').count %> Files, <%= size_for('png') / 1024 %> KB
26JPG: <%= files_for('jpg').count %> Files, <%= size_for('jpg') / 1024 %> KB

The final output will look like this

 1#!/usr/bin/env erb
 2Project Name: Web
 3
 4Direct Dependencies: 27
 5Indirect Dependencies: 109
 6Direct Initializers:  5
 7Initializers in Development Env: 119
 8Initializers in Production Env: 117
 9Controllers: 10
10Models: 16
11Views: 65
12
13
14Assets:
15JS: 15 Files, 2569 KB
16SCSS: 30 Files, 86 KB
17PNG: 42 Files, 596 KB
18JPG: 0 Files, 0 KB

Numbers will be different for your project.

Generating Graphs

The following is my approach

  1. a template that is interpreted by ERB to generate DOT format
  2. pass the output to dot command to generate another format like pdf or
    svg

So lets try to have the same data visualized, first a simple graph, lets name it rails-graph

1#!/usr/bin/env erb
2digraph graphname {
3     a -> b -> c;
4     b -> d;
5}

executing this file with

1rails-graph | dot -Tpng > graph.png

You should see the following

basic graph

Now lets put more nodes and numbers to this graph

 1#!/usr/bin/env erb
 2<%
 3stats = `bundle --local | tail -n2 | head -n1`.scan(/[0-9]+/)
 4ini = Dir.glob('config/initializers/*.rb').count
 5prod_ini = `RAILS_ENV=production rake initializers`.lines.count
 6dev_ini = `rake initializers`.lines.count
 7dev_mwares = `rake middleware`.lines.count
 8prod_mwares = `RAILS_ENV=production rake middleware`.lines.count
 9
10m = Dir.glob('app/models/**/*.*').reject{|f| f.include?('concern') }.count
11v = Dir.glob('app/views/**/*.*').count
12c = Dir.glob('app/controllers/**/*_controller.rb').count
13routes = `rake routes`.lines.count - 1
14
15def files_for(ext)
16  Dir.glob('**/*.' + ext)
17end
18def count_for(ext)
19    files_for(ext).count
20end
21def size_for(ext)
22  files_for(ext).map{|f| File.size(f) }.inject(:+).to_i
23end
24%>
25digraph graphname {
26        direct_gems [label="Direct gems <%= stats.first %>"]
27        indirect_gems [label="Indirect gems <%= stats.last %>"]
28
29        initializers [label="<%= ini  %> initializers"]
30        dev_initializers [label="<%= dev_ini %> Development initializers"]
31        prod_initializers [label="<%= prod_ini %> Production initializers"]
32
33        { rank=same; initializers dev_initializers prod_initializers }
34
35        dev_middlewares [label="<%= dev_mwares %> Development middlewares"]
36        prod_middlewares [label="<%= prod_mwares %> Production middlewares"]
37
38
39        controllers [label="Controllers: <%= c %>"]
40        models [label="Models: <%= m %>"]
41        views [label="Views: <%= v %>"]
42
43        routes [label="Routes <%= routes %>"]
44
45
46        indirect_gems -> direct_gems -> initializers -> routes
47        direct_gems -> dev_initializers -> dev_middlewares -> routes
48        direct_gems -> prod_initializers -> prod_middlewares -> routes
49
50        routes -> controllers
51
52        controllers -> models
53        controllers -> views
54
55        js [label="JS: <%= count_for('js') %> Files, <%= size_for('js') / 1024 %> KB"]
56        scss [label="SCSS: <%= count_for('scss') %> Files, <%= size_for('scss') / 1024 %> KB"]
57        png [label="PNG: <%= count_for('png') %> Files, <%= size_for('png') / 1024 %> KB"]
58        jpg [label="JPG: <%= count_for('jpg') %> Files, <%= size_for('jpg') / 1024 %> KB"]
59
60        views -> assets
61        assets -> js
62        assets -> scss
63        assets -> png
64        assets -> jpg
65
66}

The result will be as follows:
basic graph

Backlinks

See Also